Repository navigation
fix(android): Nav3 multi-Activity support now plays nicely with nav roots that don't have a SentryNavEffect - #6480
Merged
Merged
Conversation
5 of 9 tasks
📲 Install BuildsAndroid
|
This was referenced Nov 6, 2026
0xadam-brown
marked this pull request as ready for review
November 6, 2026 12:31
5 of 9 tasks
… the README (#6481) Prepares the Nav3 lifecycle stack without changing behavior. SentryNavEffect now attaches a NavRootInfo (the nav root's lifecycle plus the Activity that displays it), which later lease withdrawal needs. Renames LeasedNavState to INavStatePublisher, publisher to lastPublisher, and hasAppliedInitialBackStackUpdate to hasHandledFirstUpdate/canReceiveLease, and says "nav root" rather than "owner" throughout. The lease policy (tradeoff, handoff, diagram) moves from the NavLeaseCoordinator KDoc to the package README, so later changes to the policy are reviewed as prose.
0xadam-brown
requested review from
markushi and
runningcode
and removed request for
a team
November 11, 2026 11:11
… a nav root (#6482) A nav root that regains the nav lease (e.g., when the Activity covering it closes, the user swipes back to its ViewPager2 page, or a dialog over it is dismissed) now gets a new nav transaction for its current destination if its previous one has finished. Before, spans produced on return had no transaction to attach to until the next navigation. StagedNavTransaction keeps the destination's NavTransactionRequest after its transaction finishes or can't start, and starts a new transaction from it on the next publication. The request replaces the NavTransaction handle that recorded writes for replay, and is kept in step with back stack changes below the top via updateNavTransactionRequest.
…a SentryNavEffect (#6483) When an Activity or ViewPager2 page without a SentryNavEffect replaced the Nav3 leaseholder, the leaseholder kept its nav context and bound nav transaction, so errors and spans from the new screen were attributed to it. The coordinator now withdraws the lease in that case: it unpublishes the leaseholder's nav state and unbinds its transaction without handing the lease to another nav root. The leaseholder republishes them when it resumes. Withdrawal is triggered when another Activity resumes while the leaseholder isn't resumed (unless that Activity is itself a nav root), or when the leaseholder pauses while its host Activity stays resumed (e.g., a ViewPager2 swipe). The host Activity comes from NavRootInfo, and a NavActivityWatcher reports Activity resumes. A nav root that isn't resumed also no longer takes the lease on its first update while its host Activity is resumed (e.g., a page preloaded next to one without an effect).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5ec0c14. Configure here.
…3 transactions (#6484) The Nav3 lease is now also withdrawn when an Activity is created over the leaseholder, not only when it resumes. On API 29+, creation is reported from onActivityPreCreated, before ActivityLifecycleIntegration binds the new Activity's ui.load transaction, so the leaseholder's nav transaction no longer holds the scope's transaction slot and ui.load binds, as with Nav2. Errors and spans from the new Activity's onCreate and onStart are no longer attributed to the leaseholder on any API level. Because ui.load now binds, returning quickly to a nav root while the closed Activity's ui.load is still running would leave the nav root's transaction blocked. The coordinator therefore retries binding the leaseholder's nav transaction once when another Activity is destroyed, within 10s of the blocked attempt. Below API 29, there's no onActivityPreCreated, so the new Activity's ui.load stays unbound as before; this is documented in the package README.
markushi
approved these changes
Nov 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

PR Stack (Nav3 Lifecycle)
📜 Description
This is part 3 of our Nav3 support for multi-Activity and multi-Fragment apps.
In part 1 we provided the basic mechanism for coordinating multiple SentryNavEffects; and in part 2 we adapted our approach to the quirks of ViewPager2 and other oddball Fragment lifecycles.
Both of those PR stacks assumed that any Activities or Fragments that had a SentryNavEffect would only launch other Activities or Fragments that also had SentryNavEffects. This PR stack handles three remaining (and important) corner cases:
ui.loadtransactions interoperate well with Nav3 transactions. (Nav3 transactions now consistently defer toui.loadtransactions, like we do for Nav2.)Background.
A nav root is an Activity or Fragment that owns a nav back stack and passes it to a SentryNavEffect. (An Activity or Fragment that doesn't have a SentryNavEffect is called a non-candidate root.) When the user navigates to a new destination, SentryNavEffect records the screen name, the nav context and one nav transaction.
Each nav root has its own SentryNavEffect instance and all instances write to the same
IScopewith a single transaction slot. Auto-instrumented spans (HTTP, database, and so on) attach to whatever transaction is in that slot. With multi-Activity or multi-Fragment apps, several nav roots can be alive at the same time (e.g., an Activity covered by another Activity, or neighboring ViewPager2 pages), meaning SentryNavEffect is subject to the classical headaches of shared global state.NavLeaseCoordinatorwas (and remains) our answer to those headaches. It prevents multiple, simultaneously active SentryNavEffects from clobbering each others' updates via a nav lease, viz., by only allowing the nav root holding the lease to write nav data to the IScope or bind to its transaction slot. When the lease is transferred, the old holder's data is removed and its transaction is unbound but left running.The gaps in our existing implementation
Our original lease model left three gaps that could cause stale nav data to be left on the shared IScope as the user moved around the host app:
SentryNavEffectui.loadtransaction†ui.loadbinds in preference to the nav transaction, as it does with Nav2 (#6484)Unchanged
sentry-android-coreisn't modified. #6484 works aroundActivityLifecycleIntegration's callback timing instead.Known limitations
All limitations are documented in the package README.
[1] A
ui.actionfrom the gesture that caused a return can still block the nav transaction.[2] We rely on the pausing of the nav root holding the lease in order to remove its nav data + transaction from the shared IScope in situations where a non-candidate root covers the leaseholder. If the nav root can be covered without being paused, we won't know about it and we won't remove its (arguably stale) data from the IScope. That happens with Fragment dialogs, Compose dialogs, and bottom sheets.
[3] Below API 29, an Activity opened from a nav root still doesn't get its
ui.loadtransaction bound and we bind the nav transaction instead. Theui.loadtransaction is created and sent in both cases, but it's not bound to the transaction for pre-API 29 devices, and so it won't be the parent of any HTTP, SQLite or other ambient spans (the nav transaction will be instead).Note: Updating pre-API 29 devices to match requires modifying
ActivityLifecycleIntegration. I haven't included that change here because it would have impacts beyond Nav3. We could do it in a follow-up if folks care to. Nicely, it'd let us simplify the API 29+ approach implemented here, as well.💡 Motivation and Context
Absent this PR stack, apps that mix Nav3 with plain Activities, ViewPager2 or other Activities could see stale nav data or spans attributed to the wrong screen. They could also lose spans produced when the user returned to a screen, and see
ui.loadbehavior that differs from Nav2.See the originating Slack convo here.
addresses: JAVA-823 and JAVA-785
💚 How did you test it?
See the individual PRs.
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
#skip-changelog
Merge the stack PRs bottom to top using merge commits, then squash-merge this PR.